8장. 예외처리

항목 39: 예외는 예외상황에서만 써라

예외는 말 그대로 예외상황에서만 써야 한다. 프로그램 흐름을 예외로 제어하려 하면 안 된다.
API설계 시 프로그램 흐름을 제어할 때 예외를 쓸 수 밖에 없도록 만들지 않는다.

  • "상태 의존" 메소드가 있다면 "상태 검사" 메소드도 함께 제공해야 한다.
    예를 들면, Iterator 클래스의 next()와 hasNext()메소드 등




    이렇게 절대 하지 마라

try {
    int i = 0;
    while(true) 
        a[i++].f();
} catch(ArrayIndexOutOfBoundsException e) {
}

이렇게 바꿔 써라


for (int i = 0; i < a.length; i++)
    a[i].f();

  • 표준 반복문 구현 패턴에서 반복 종료 조건을 또 한 번 확인하는 것은 중복 작업이다.

    따라서 제거하는 것이 마땅하다
    • 예외처리의 성능까지 최적화하려는 JVM은 거의 없고, 예외를 생성하고 던지고 잡는 것은 비용이 많이 드는 작업이다.
    • 최신 JVM이 수행하는 성능 최적화 대상인 코드라도 try-catch 안에 있으면 최적화에서 빠질 수 있다.
    • 표준 구현패턴을 써도 JVM이 유효한 배열범위를 항상 검사하는 것은 아니다. 최신 JVM 은 이 검사마저도 최적화 과정에서 없앤다.
  • 예외 기반의 구현 패턴의 문제점
    • 코드의 목적을 애매하게 만들고 성능을 떨어뜨린다.
    • 다른 버그가 있어도 이 버그를 감춘 채 계속 수행된다.(제대로 동작하리라는 보장이 없다)

항목 40: 처리해야 하는 예외와 런타임 예외를 구분해서 던져라

  • 처리해야 하는 예외(checked exception)
  • 런타임 예외(run-time exception)
  • 에러(error)

처리해야 하는 예외와 처리하지 않는 예외를 결정하는 규칙

처리해야 하는 예외(checked exception)

  • 호출자가 예외 상황을 복구할 수 있다고 기대할 수 있을 때 던진다.
    • 호출자에게 이 예외를 catch 구문으로 잡아서 처리하거나 다시 던져 외부로 전파시킬 것을 강요하는 것이다.
    • 물론, 예외를 잡아서 무시하는 방법으로 요구를 묵살할 수도 있지만 이것은 잘못된 생각이다(항목 47)

처리하지 않는 예외(unchecked exception)

  • 런타임 예외(run-time exception)와 에러(error)가 있다 - 이 둘의 행동방식은 똑같다.
    • 잡을(catch) 필요도 없고 특별한 경우가 아니라면 잡아서도 안 된다.
    • 복구할 수 없는 상황이므로 더 이상 프로그램을 실행하는 것은 위험할 수 있다.
    • 이 문제가 발생한 스레드는 적절한 오류 메시지를 내고 멈추기 때문에 위험을 피할 수 있다.
    • 위험을 해결할 수 없다면 피해야 한다
  • 런타임 예외는 프로그래밍 오류가 발생했을 때만 써야 한다.
    • 사전 조건을 어겼을 때 발생한다.
    • 배열에 접근할 때 지켜야 할 사전조건은 인덱스는 0부터 배열크기-1 까지가 범위이다.
      만약 이 조건을 어기면 ArrayIndexOutOfBoundsException 이 발생한다.
    • 처리하지 않는 예외는 모두 Runtime-Exception의 하위 클래스이어야 한다.
  • JLS(Java Language Specification) 에 정해놓은 사항은 아니지만 에러는 JVM의 자원이 부족하거나,
    불변규칙이 변하는 것과 같이 더 이상 프로그램을 진행할 수 없을 때 JVM에서만 던지는 것이 관례이다.
  • Exception, RunTimeException, Error의 하위클래스가 아닌 "던질 수 있는 것"인 Throwable의 하위클래스를 정의할 수도 있다.
    그럼 도대체 언제 이런 괴물을 써야 하나? 절대 쓰지 마라 다른 예외와 비교했을 때 좋은 게 하나도 없다.

정리

복구 가능한 상황이라면 처리해야 하는 예외를 쓰고 프로그래밍 오류가 발생한 상황이라면 런타임 예외를 써라.
만약, 판단하기 힘들다면 처리하지 않는 예외를 던지는 것이 더 좋다(항목 41)

항목 41: 처리해야 하는 예외는 꼭 필요한 때만 던져라

처리해야 하는 예외를 던지는 메소드를 호출하는 메소드는 catch 블록을 써서 이 예외를 잡거나,
같은 예외를 던진다고 선언하여 이 예외를 외부로 전파해야 한다.
프로그래머가 예외를 적절하게 처리할 수 없을 때는 처리하는 않는 예외를 던지는 것이 좋다


} catch(TheCheckedException e) {
    throw new Error("Assertion error");  // 절대 일어나서는 안 되는 일
}


} catch(TheCheckedException e) {
    e.printStackTrace();
    System.exit(1);  // 헉, 졌다
}

  • API 사용자가 TheCheckedException을 이렇게 처리할 수 밖에 없다면, 처리하지 않는 예외를 던지는 편이 더 낫다.
    • CloneNotSupportedException이 좋은 예다.
  • 전체 코드에서 닥 한 곳에서만 처리해야 하는 예외가 발생할 수 있다면,
    단지 이것 때문에 try 블록을 써야 한다. 이런 상황이라면 이 메소드가 꼭 처리해야 하는 예외를 던져야 하는 지 다른
    방법은 없는지 생각해봐야 한다.
  • 처리해야 하는 예외를 처리하지 않는 예외로 바꾸는 방법이 있다.

// 처리해야 하는 예외
try {
    obj.action(args);
} catch(TheCheckedException e) {
    // 예외상황을 처리한다
    ...
}

이렇게 바꾼다.


// 상태 검사 메소드와 처리하는 않는 예외
if (obj.actionPermitted(args)) {
    obj.action(args);
} else {
    // 예외상황을 처리한다.
    ...
}

실패하더라도 스레드가 끝나 버리면 그만이라고 판단하다면 다음과 같이 더 간단하게 만들 수 있다


obj.action(args);

예외가 발생하는 상황인지 확인하는 메소드와 실제 작업을 처리하는 메소드로 나눈 것은 "상태 검사 메소드"(항목 39)
와 같은 방식이다.

주의할 점

외부에서 동기화하지 ?고 동시에 이 메소드가 호출되는 객체를 쓰거나, 내부상태를 바꿀 수 있다면
메소드를 2개로 나누지 말아야 한다.
actionPermitted 메소드를 호출한 시점과 action메소드를 호출한 시점 사이에 객체의 상태가 바뀔 수 있기 때문이다.

코드41.1 Main.java 처리해야 하는 예외를 상태 검사 메소드와 처리하지 않는 예외로 바꾼다


public class Main41 {
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Main41 obj = new Main41();
		
		String test = new String("Hello");
		
		if (obj.actionPermitted(test)) {
			obj.action(args);
		} else {
			System.out.println("Action not permitted");
		}
	}

	public void action(String[] args) {
		System.out.println("action.permitted");
	}

	boolean actionPermitted(Object obj) {
		if (obj instanceof Main41)
			return true;
		return false;
	}
}

항목 42: 표준 예외를 써라

자바 플랫폼 라이브러리는 대부분 API에서 쓸 수 있도록 처리하지 않는 예외의 기본형을 거의 다 제공한다.
이런 예외들을 살펴본다.

  • 기존 예외 재사용의 장점
    1. API를 쉽게 배울 수 있고 쉽게 쓸 수 있다.
    2. 가독성이 좋아진다.
    3. 예외 클래스가 적을수록 메모리 사용과 초기 클래스 로딩 시간이 줄어든다.
  • IllegalArgumentException
    호출자가 전달한 인자값이 적절치 않을 때 이 예외를 던진다.
  • IllegalStatementException
    메소드를 호출받은 객체의 상태가 메소드르 수행할 만한 상태가 아닐 때 이 예외를 던진다.
    완전 초기화되지 않은 객체에 접근할 때 발생한다.
  • ConcurrentModificationException
    단일 스레드에서만 쓰거나 이부에서 동기화한 상태에서만 쓸 수 있도록 설계한 객체에
    여러 스레드가 동시에 접근해서 이 객체를 수정하거나 이미 수정하였다면 이 예외를 던진다.
  • UnsupportedOperationException
    어떤 객체가 요청받은 작업을 지원하지 않을 때 이 예외를 던진다.
    구성요소를 추가만 할 수 있는 List 구현체를 만들었다면 삭제하는 메소드는 이 예외를 던져야 한다.

public abstract class LogComponent {
	public abstract String getName();
	public abstract Integer getCount();
	
	public LogComponent add(LogComponent logComponent) throws UnsupportedOperationException {
		throw new UnsupportedOperationException();
	}
         ...
}

public class LogNode extends LogComponent {
	
	private static final String DELI = "/";
	
	private String name;
	private List<LogComponent> logList = new ArrayList<LogComponent>();
	
	public LogNode(String name) {
		this.name = name;
	}
	
	public LogComponent add(LogComponent logComponent) {
		logList.add(logComponent);
		return this;
	}
         ...
}

public class LogLeaf extends LogComponent {
	
	private static final String DELI = "/";
	
	private String name;
	private Integer count;
	
	public LogLeaf(String name, Integer count) {
		this.name = name;
		this.count = count;
	}

	@Override
	public Integer getCount() {
		return count;
	}
         ...
}


항목 43: 예외를 적절하게 추상화하라 - 예외 변환(exception translation)

높은 계층에서 낮은 계층의 예외를 잡아서 높은 계층의 추상화 수준에 맞게 변환해서 던져야 한다. - 예외 변환이라고 한다.
남용하지 않게 조심해야 한다.


try {
    // 낮은 계층의 추상화를 써서 작업을 처리한다.
    ...
} catch(LowerLevelException e) {
    throw HigherLevelException(...);
}


public Object get(int index) {
    ListIterator i = listIterator(index);
    try {
        return i.next();
    } catch(noSuchElementException e) {
        throw new IndexOutOfBoundsException("Index: " + index);
    }


public void batchInsertRefererKeyword(final String period, final List<RefererKeywordSet> refererKeywordSets)
		throws DataAccessException {
	getBlogstatSqlMapClientTemplate().execute(new SqlMapClientCallback() {
		public Object doInSqlMapClient(SqlMapExecutor executor)
				throws SQLException {
			executor.startBatch();
			for (int i = 0; i < refererKeywordSets.size(); i++) {
				try {
					insertRefererKeyword(period, refererKeywordSets.get(i));
				} catch (Exception e) {
					RefererKeywordSet keyword = refererKeywordSets.get(i);
					throw new LoadException(keyword.toString(), e);
				}
			}
			@SuppressWarnings("unused")
			int insCount = executor.executeBatch();
			return null;
		}
	});
}

항목 44: 메소드가 던지는 모든 예외를 명세문서에 기술하라

각 메소드가 던지는 모든 예외를 신중하게 문서화해야 한다.

  • 처리해야 하는 예외는 메소드 선언부에 하나씩 선언하고 @throws 태그를 써서 모든 예외가 발생하는
    상황을 정확하게 문서화하라
  • 어떤 메소드가 여러 개의 예외를 던질 수 있을 때 공통 상위타입 예외로 던지지 마라
  • Exception을 던질 수 있다고 선언하거나, Throwable을 던질 수 있다고 선언한 메소드는 절대로 없어야 한다.
  • 처리하지 않는 예외를 설명하는 가장 좋은 방법은 사전 조건을 기술하는 것이다.
  • 처리하지 않는 예외는 @throws 태그를 써서 명세문서에 기술하지만, 메소드 선언의 throws절에 나타나지 말아야 한다.
  • 대부분 메소드가 같은 예외를 던지는 클래스라면 각 메소드의 문서화 주석에 기술하는 것보다
    클래스의 문서화 주석에 기술하는 것이 좋다. ex) NullPointerException

항목 45: 실패에 대한 자세한 정보를 상세 메시지 문자열에 담아라

  • 실패 원인을 포착하려면, 예외의 문자열 표현에 반드시 예외 발생에 영향을 준 모든 필드와 인자의 값이 들어 있어야 한다.
    • IndexOutOfBoundsException은 인덱스의 상한값, 하한값, 예외가 발생한 시점의 인덱스값이 들어 있어야 한다.
  • 세부 메시지를 문자열 인자로 받는 생성자 대신 필요한 정보 자체를 인자로 받는 생성자를 제공하는 것이 좋다.
    • IndexOutOfBoundsException 은 String생성자 대신에 다음과 같은 생성자를 제공할 수 있을 것이다.

	/**
	 * IndexOutOfBoundsException 을 생성한다.
	 * @param lowerBound 정당한 최하위 인덱스 값
	 * @param upperBound 정당한 최상위 인덱스 값에 1을 더한 값
	 * @param index 실제 인덱스 값
	 */
	public IndexOutOfBoundsException(int lowerBound, int upperBound,
			int index) {
		// 실패를 자세히 설명할 수 있는 세부 메시지를 만들어라.
		supper( "Lower bound: " + lowerBound +
				", Upper bound: " + upperBound +
				", Index: " + index);
	}

항목 46: 실패 원자성을 얻기 위해 노력하라

예외를 던지고 난 객체는 어떤 상태여야 할까? 객체상태는 분명하고 다시 쓸 수 있는 것이 바람직하다.
메소드 호출이 실패하더라도 객체상태는 메소드 호출 전과 같아야 한다

  • 실패 원자성이 있는(failure atomic) 메소드
  • 실패 원자성을 가질 수 있게 하는 방법
    • 불변 객체를 만든다(항목 13)
    • 가변 객체의 경우, 메소드 본체에서 작업을 처리하여 객체 상태를 변경하기 전에 인자의 유효서을 검사하여
      예외를 던지면 된다(항목 23)

public Object pop() {
    if (size == 0)
        throw new EmptyStackException();
    Object result = element[--size];
    element[size] = null;  // 쓸모 없는 참조 제거
    return result;

    • 실패가 발생할 가능성이 있는 부분을 객체의 값을 수정하는 부분보다 먼저 처리하는 것이다.
      TreeMap의 경우 값을 넣지 전에 서로 비교하는 데 서로 비교할 수 없는 타입인 경우, ClassCastException이 발생하면서
      트리가 수정되기 전에 자연스럽게 실패할 것이다.
    • 객체의 임시 본사본을 만들어 이 객체에 처리를 수행하고 완료되면 원본 객체를 임시 복사본 객체로 바꾼다.

      Warning
      2개의 스레드를 동기화하지 않은 채 같은 객체를 동시에 수정한다면 이 객체의 상태는 이상해진다.
      ConcurrentModificationException을 잡은 후에도 이 객체는 계속 쓸 수 있다고 생각하면 오산이다.
      error는 exception과 달리 복구할 수 없기 때문에 오류가 발생했을 때 실패 원자성을 달성하기 위해 애쓸 필요가 없다.

항목 47: 예외를 잡아서 버리지 마라

가령, 메소드 호출을 try 블록으로 둘러싸고는 빈 catch 블록을 써서 예외를 무시하는 경우가 많은데
의심해 볼 만하다.
예외를 잡아서 버리면 프로그램은 오류가 발생해도 아무 일 없듯이 잘 진행되는 것 처럼 보인다.

catch 블록 안에서 정말 아무것도 할 것이 없다면 최소한 왜 예외를 잡아서 처리하지 않고
버리는지 그 이유라도 주석으로 달아 놓아야 한다.

문서에 대하여